use crate::REFERENCE;

use super::Invalid;
use std::{ops::Range, str::FromStr};

pub use Month::*;
pub use Weekday::*;

def_enum! {
    pub enum Weekday(u8) {
        Sunday = 0 | 7, Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6,
    }
}

def_enum! {
    pub enum Month(u8) {
        January = 1, February = 2, March = 3, April = 4, May = 5, June = 6,
        July = 7, August = 8, September = 9, October = 10, November = 11, December = 12 | 0,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Year(pub(super) u16);

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Day(pub(super) u8);

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
    pub year: Year,
    pub month: Month,
    pub day: Day,
}

impl Year {
    pub fn from_u16(year: u16) -> Self {
        Year(year)
    }
    pub fn first_day(self) -> Date {
        Date {
            year: self,
            month: Month::January,
            day: Day(1),
        }
    }
    pub fn last_day(self) -> Date {
        Date {
            year: self,
            month: Month::December,
            day: Day(31),
        }
    }
    pub fn is_leap(self) -> bool {
        self.0 % 4 == 0 && (self.0 % 100 != 0 || self.0 % 400 == 0)
    }

    pub fn days(self) -> u32 {
        if self.is_leap() {
            366
        } else {
            365
        }
    }

    pub fn next(self) -> Self {
        Year(self.0 + 1)
    }

    pub fn last(self) -> Self {
        Year(self.0 - 1)
    }
}

impl Month {
    pub fn days(self, year: Year) -> u8 {
        match self {
            January | March | May | July | August | October | December => 31,
            April | June | September | November => 30,
            February if year.is_leap() => 29,
            February => 28,
        }
    }
}

impl Date {
    pub fn try_new(year: u16, month: u8, day: u8) -> Result<Self, InvalidDate> {
        let year = Year(year);
        let month = Month::try_from(month).map_err(|_| InvalidDate::Month)?;
        if !(1..=month.days(year)).contains(&day) {
            return Err(InvalidDate::Day);
        }
        let day = Day(day);
        Ok(Date { year, month, day })
    }
    pub fn new(year: u16, month: u8, day: u8) -> Self {
        Self::try_new(year, month, day).unwrap()
    }
    pub fn is_year_start(self) -> bool {
        self.month == Month::January && self.day.0 == 1
    }
    pub fn is_year_end(self) -> bool {
        self.month == Month::December && self.day.0 == 31
    }
    pub fn is_month_start(self) -> bool {
        self.day.0 == 1
    }
    pub fn is_month_end(self) -> bool {
        self.day.0 == self.month.days(self.year)
    }
    pub fn weekday(self) -> Weekday {
        REFERENCE.weekday + self.days_since(REFERENCE.date())
    }
    pub fn next_weekday(mut self, weekday: Weekday) -> Date {
        let mut day = self.weekday();
        while day != weekday {
            day += 1;
            self = self.next_day();
        }
        self
    }
    pub fn prev_weekday(mut self, weekday: Weekday) -> Date {
        let mut day = self.weekday();
        while day != weekday {
            day -= 1;
            self = self.prev_day();
        }
        self
    }
    pub fn next_year(self) -> Self {
        self.year.next().first_day()
    }
    pub fn prev_year(self) -> Self {
        self.year.last().last_day()
    }
    pub fn next_day(mut self) -> Self {
        if self.is_month_end() {
            self.next_month()
        } else {
            self.day.0 += 1;
            self
        }
    }
    pub fn prev_day(mut self) -> Self {
        if self.is_month_start() {
            self.prev_month()
        } else {
            self.day.0 -= 1;
            self
        }
    }
    pub fn next_month(mut self) -> Self {
        if self.is_year_end() {
            self.next_year()
        } else {
            self.month += 1;
            self.day.0 = 1;
            self
        }
    }
    pub fn prev_month(mut self) -> Self {
        if self.is_year_start() {
            self.prev_year()
        } else {
            self.month -= 1;
            self.day.0 = self.month.days(self.year);
            self
        }
    }
    pub fn nth_week_since_year_start(self) -> u8 {
        if self > self.next_year().prev_weekday(Sunday) {
            return 1;
        }
        // The first week is the week that contains the first Thursday of the year.
        let ref_day = self
            .year
            .first_day()
            .next_weekday(Thursday)
            .prev_weekday(Sunday);
        ((self.days_between(ref_day) - 1) / 7) as u8 + 1
    }
    pub fn days_since(self, earlier: Date) -> u32 {
        assert!(self >= earlier);
        (earlier.year.0..self.year.0)
            .map(Year)
            .map(Year::days)
            .sum::<u32>()
            + self.days_since_year_start()
            - earlier.days_since_year_start()
    }
    pub fn after_days(mut self, mut days: u32) -> Self {
        while days > 0 {
            let days_to_next_year = self.days_until_year_end() + 1;
            if days < days_to_next_year {
                break;
            }
            days -= days_to_next_year;
            self = self.next_year();
        }
        while days > 0 {
            let days_to_next_month = u32::from(self.month.days(self.year) - self.day.0 + 1);
            if days < days_to_next_month {
                break;
            }
            days -= days_to_next_month;
            self = self.next_month();
        }
        self.day.0 += days as u8;
        self
    }
    pub fn days_between(self: Date, other: Date) -> i32 {
        if self >= other {
            self.days_since(other) as i32
        } else {
            -(other.days_since(self) as i32)
        }
    }
    pub fn days_since_year_start(self) -> u32 {
        MonthIter(1..self.month.into())
            .map(|month| u32::from(month.days(self.year)))
            .sum::<u32>()
            + u32::from(self.day.0 - 1)
    }
    pub fn days_until_year_end(self) -> u32 {
        self.year.days() - self.days_since_year_start() - 1
    }
}

impl From<super::年> for Year {
    fn from(年: super::年) -> Self {
        Year(年.年)
    }
}

pub struct MonthIter<T>(T);

impl<T: Iterator<Item = u8>> Iterator for MonthIter<T> {
    type Item = Month;

    fn next(&mut self) -> Option<Month> {
        let month = self.0.next()?;
        Month::try_from(month).ok()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.0.size_hint()
    }
}

pub struct DateIter(Range<Date>);

impl Iterator for DateIter {
    type Item = Date;

    fn next(&mut self) -> Option<Date> {
        let DateIter(Range { start, end }) = self;
        if start >= end {
            return None;
        }
        let date = *start;
        *start = start.next_day();
        Some(date)
    }
}

impl_add_sub_modulo!(Weekday, 7, (u8, i8), (u16, i16), (u32, i32));
impl_add_sub_modulo!(Month, 12, (u8, i8), (u16, i16), (u32, i32));

pub type InvalidWeekday = Invalid<Weekday>;
pub type InvalidMonth = Invalid<Month>;
#[derive(Debug)]
pub enum InvalidDate {
    Year,
    Month,
    Day,
}

impl FromStr for Date {
    type Err = InvalidDate;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.split('-');
        let year = parts
            .next()
            .and_then(|s| s.parse().ok())
            .ok_or(InvalidDate::Year)?;
        let month = parts
            .next()
            .and_then(|s| s.parse().ok())
            .ok_or(InvalidDate::Month)?;
        let day = parts
            .next()
            .and_then(|s| s.parse().ok())
            .ok_or(InvalidDate::Day)?;
        Ok(Date::new(year, month, day))
    }
}

impl_display!(Year = |self| Display(self.0));
impl_display!(Month = |self| Display(u8::from(*self)));
impl_display!(Day = |self| Display(self.0));
impl_display!(Weekday = Debug);
impl_display!(Date = Debug);
impl_debug!(Date = |self, f| write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day));

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_after_days() {
        let date = Date::new(2024, 1, 1);
        assert_eq!(date.after_days(0), Date::new(2024, 1, 1));
        assert_eq!(date.after_days(40), Date::new(2024, 2, 10));
    }

    #[test]
    fn test_days_since() {
        let date = Date::new(2024, 1, 1);
        assert_eq!(Date::new(2024, 1, 1).days_since(date), 0);
        assert_eq!(Date::new(2024, 2, 10).days_since(date), 40);
    }
}
